﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace VSDoc2JsDoc
{
    class VsDocTransform
    {
        private BBCodeFilter filter = new BBCodeFilter();

        public string Apply(string fileContents, JsScript script)
        {
            List<Tuple<int, object>> inserts = new List<Tuple<int, object>>();
            List<Tuple<int, object>> removes = new List<Tuple<int, object>>();

            foreach (ClassDefinition classDef in script.ClassDefinitions)
            {
                string lends = String.Format("/** @lends {0}.prototype */" + Environment.NewLine, classDef.ClassName);
                inserts.Add(new Tuple<int, object>(classDef.StartToken.StartIndex, lends));

                MemberDefinition constructor = classDef.MemberDefinitions.FirstOrDefault(md => md.IsConstructor);
                if (constructor == null)
                {
                    constructor = new MemberDefinition()
                    {
                        MemberName = "constructor"
                    };

                    classDef.MemberDefinitions.Add(constructor);
                    HandleConstructor(classDef);
                    string funcdef = @"constructor: function() { }" + (classDef.MemberDefinitions.Count > 1 ? ",\n" : "");
                    inserts.Add(new Tuple<int, object>(classDef.StartToken.StartIndex + 1, String.Format("/**\n {0}\n*/\n{1}", constructor.JsDoc, funcdef)));
                    classDef.MemberDefinitions.Remove(constructor);
                }
                else
                {
                    HandleConstructor(classDef);
                }


                foreach (MemberDefinition memberDef in classDef.MemberDefinitions)
                {
                    ApplyGeneralTags(memberDef);
                    inserts.Add(new Tuple<int, object>(memberDef.StartToken.StartIndex, memberDef.JsDoc));
                    removes.Add(new Tuple<int, object>(Int32.Parse(memberDef.VsDoc.Attribute("startChar").Value), Int32.Parse(memberDef.VsDoc.Attribute("length").Value)));
                }
            }


            StringBuilder sb = new StringBuilder(fileContents);
            int offset = 0;
            var list = inserts.Concat(removes).OrderBy(i => i.Item1).Where(i => !String.IsNullOrEmpty(i.Item2.ToString()));
            foreach (var t in list)
            {
                if (t.Item2 is string)
                {
                    sb.Insert(offset + t.Item1, t.Item2);
                    offset += ((string)t.Item2).Length;
                }
                else
                {
                    sb.Remove(offset + t.Item1, (int)t.Item2);
                    offset -= (int)t.Item2;
                }
            }

            return sb.ToString();
        }

        private void HandleConstructor(ClassDefinition classDef)
        {
            MemberDefinition constructor = classDef.MemberDefinitions.FirstOrDefault(md => md.IsConstructor);

            XElement doc = constructor.VsDoc;
            StringBuilder sb = new StringBuilder();

            if (!String.IsNullOrEmpty(classDef.BaseClassName))
            {
                sb.AppendLine("@extends " + classDef.BaseClassName);
            }

            sb.Append("@class ");

            XElement description = doc.Element("description");
            if (description != null)
            {
                sb.AppendLine(filter.ToHtml(description.Value));
            }
            else
            {
                sb.AppendLine();
            }

            sb.AppendLine();
            sb.AppendLine("@constructs");

            foreach (XElement field in doc.Elements("field").ToArray())
            {
                string memberName = field.Attribute("name").Value;
                
                MemberDefinition member = classDef.MemberDefinitions.FirstOrDefault(md => md.MemberName == memberName);

                field.Remove();

                if (member == null)
                {
                    XAttribute fieldType = field.Attribute("type");

                    if (fieldType == null)
                    {
                        sb.AppendFormat("@property {0} {1}" + Environment.NewLine, memberName, filter.ToHtml(field.Value));
                    }
                    else
                    {
                        sb.AppendFormat("@property {{{0}}} {1} {2}" + Environment.NewLine, fieldType.Value, memberName, filter.ToHtml(field.Value));
                    }
                }
                else
                {
                    member.VsDoc.Add(field);
                }
            }

            constructor.JsDoc = sb.ToString();
        }

        private void ApplyGeneralTags(MemberDefinition memberDef)
        {
            StringBuilder sb = new StringBuilder(memberDef.JsDoc);

            var signatures = memberDef.VsDoc.Elements("signature").ToArray();

            if (memberDef.IsConstructor)
            {
                ApplySignature(0, memberDef, sb, signatures.Any() ? signatures.First() : memberDef.VsDoc);
                sb.Insert(0, "/**" + Environment.NewLine);
                sb.AppendLine(Environment.NewLine + "*/");
            }
            else
            {
                if (signatures.Any())
                {
                    for (int i = 0; i < signatures.Length; i++)
                    {
                        StringBuilder jsdoc = new StringBuilder();
                        ApplySignature(i, memberDef, jsdoc, signatures[i]);

                        jsdoc.Insert(0, "/**" + Environment.NewLine);
                        jsdoc.AppendLine(Environment.NewLine + "*/");

                        sb.AppendLine(jsdoc.ToString());
                    }
                }
                else
                {
                    ApplySignature(0, memberDef, sb, memberDef.VsDoc);
                    sb.Insert(0, "/**" + Environment.NewLine);
                    sb.AppendLine(Environment.NewLine + "*/");
                }
            }

            memberDef.JsDoc = sb.ToString();
        }

        private void ApplySignature(int number, MemberDefinition memberDef, StringBuilder jsdoc, XElement vsdoc)
        {
            XElement field = vsdoc.Element("field");
            if (field != null)
            {
                jsdoc.Insert(0, filter.ToHtml(field.Value) + Environment.NewLine + Environment.NewLine);

                XAttribute isStatic = field.Attribute("static");
                if (isStatic != null && Boolean.Parse(isStatic.Value))
                {
                    jsdoc.AppendFormat("@name {0}.{1}" + Environment.NewLine, memberDef.ClassDefinition.ClassName, memberDef.MemberName);
                }
                else
                {
                    jsdoc.AppendFormat("@name {0}.prototype.{1}" + Environment.NewLine, memberDef.ClassDefinition.ClassName, memberDef.MemberName);
                }

                XAttribute type = field.Attribute("type");
                if (type != null)
                {
                    jsdoc.AppendLine("@type " + type.Value);
                }

                return;
            }

            XElement sum = vsdoc.Element("summary");
            if (sum != null)
            {
                jsdoc.Insert(0, filter.ToHtml(sum.Value) + Environment.NewLine + Environment.NewLine);
            }

            if (!memberDef.IsConstructor)
            {
                jsdoc.AppendFormat("@name {0}.prototype.{1}{2}" + Environment.NewLine, memberDef.ClassDefinition.ClassName, memberDef.MemberName, number == 0 ? "" : ("^" + (number + 1)));
                if (memberDef.IsFunction)
                {
                    jsdoc.AppendLine("@function");
                }
            }

            ApplyParam(jsdoc, vsdoc);

            XElement returns = vsdoc.Element("returns");
            if (returns != null)
            {
                XAttribute retType = returns.Attribute("type");
                if (retType == null)
                {
                    jsdoc.AppendFormat("@returns {0}" + Environment.NewLine, filter.ToHtml(returns.Value));
                }
                else
                {
                    jsdoc.AppendFormat("@returns {{{0}}} {1}" + Environment.NewLine, retType.Value, filter.ToHtml(returns.Value));
                }
            }

            foreach (XElement example in vsdoc.Elements("example"))
            {
                jsdoc.AppendLine();

                jsdoc.Append("@example ");
                jsdoc.AppendLine(UnIndentExample(example.Value));
            }
        }

        private string UnIndentExample(string example)
        {
            List<String> lines = example.Split('\n').ToList();
            StringBuilder sb = new StringBuilder();
            int cols = lines.Max(l => l.Length);
            int baseIndent = 0;

            // trim empty lines
            while (lines.Count > 0 && String.IsNullOrWhiteSpace(lines[0])) lines.RemoveAt(0);
            while (lines.Count > 0 && String.IsNullOrWhiteSpace(lines[lines.Count - 1])) lines.RemoveAt(lines.Count - 1);

            for (int col = 0; col < cols; col++)
            {
                bool wsOnly = true;
                for (int line = 0; line < lines.Count; line++)
                {
                    if (col < lines[line].Length)
                    {
                        char c = lines[line][col];
                        if (!Char.IsWhiteSpace(c))
                        {
                            wsOnly = false;
                        }
                    }
                }

                if (!wsOnly)
                {
                    break;
                }

                baseIndent++;
            }

            foreach (var line in lines)
            {
                if (String.IsNullOrWhiteSpace(line))
                {
                    sb.AppendLine();
                }
                else
                {
                    sb.AppendLine(line.Substring(Math.Min(line.Length - 1, baseIndent)));
                }
            }

            sb.Replace("\t", "    ");
            return sb.ToString();
        }

        private void ApplyParam(StringBuilder jsdoc, XElement vsdoc)
        {
            foreach (XElement param in vsdoc.Elements("param"))
            {
                string type = null;
                string description = String.Empty;

                XAttribute typeAttr = param.Attribute("type");
                if (typeAttr != null)
                {
                    type = typeAttr.Value;
                }

                
                XElement signature = param.Element("signature");
                if (signature != null)
                {
                    type = CallbackParam(signature);
                    XElement desc = param.Element("summary");
                    if (desc != null)
                    {
                        description = filter.ToHtml(desc.Value);
                    }
                }
                else
                {
                    description = filter.ToHtml(param.Value);
                }

                if (signature == null && param.Element("param") != null || type == "Function")
                {
                    type = CallbackParam(param);
                }

                string paramName = param.Attribute("name").Value;
                XAttribute opt = param.Attribute("optional");
                if (opt != null && Boolean.Parse(opt.Value))
                {
                    paramName = "[" + paramName + "]";
                }

                if (!String.IsNullOrEmpty(type))
                {
                    jsdoc.AppendFormat("@param {{{0}}} {1} {2}" + Environment.NewLine, type, paramName, description);
                }
                else
                {
                    jsdoc.AppendFormat("@param {0} {1}" + Environment.NewLine, paramName, description);
                }
            }
        }

        private string CallbackParam(XElement signature)
        {
            string[] formalParams = signature.Elements("param")
                .Select(p => p.Attribute("type") == null ? p.Attribute("name").Value : String.Format("{0}&nbsp;{1}", p.Attribute("type").Value, p.Attribute("name").Value)).ToArray();
            return String.Format("function({0})", String.Join(",&nbsp;", formalParams));
        }
    }
}
